package com.guodi.common.utils;

import com.guodi.common.dto.DownloadZipDTO;
import com.guodi.common.dto.ZipEntityDTO;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.tools.zip.ZipFile;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * @描述: 压缩工具类
 * @作者: 余家鑫
 * @日期: 2021/3/19 上午9:47
 */
public class ZipUtil {

    /**
     * @描述: 解压缩
     * @入参: zipStream zip的压缩流
     * @入参: outFile 输出文件路径
     * @出参:
     * @作者: 余家鑫
     * @日期: 2021/3/19 上午9:54
     */
    public static File unzip(ZipInputStream zipStream, File outFile){
        try {
            ZipEntry zipEntry;
            File outItemFile;
            while (null != (zipEntry = zipStream.getNextEntry())) {
                outItemFile = FileUtil.file(outFile, zipEntry.getName());
                if (zipEntry.isDirectory()) {
                    // 目录
                    outItemFile.mkdirs();
                } else {
                    // 文件. 从流读取
                    FileUtil.writeFromStream(zipStream, outItemFile);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (zipStream != null) {
                try {
                    zipStream.close();
                } catch (final IOException ignored) { // NOPMD
                }
            }
        }
        return outFile;
    }

    /**
     * @描述: 解压缩
     * @入参:
     * @出参:
     * @作者: 余家鑫
     * @日期: 2021/3/19 上午10:15
     */
    public static File unzip(InputStream zipStream, File outFile){
        return unzip(new ZipInputStream(zipStream, Charset.forName("GBK")), outFile);
    }

    /**
     * 压缩文件
     * @入参： zipEntityDTOList 文件列表
     * @入参： outputStream 输出流，会自动关闭
     * @throws Exception
     */
    public static void compressZipToOutputStream(List<ZipEntityDTO> zipEntityDTOList, OutputStream outputStream) throws Exception {
        try(ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
            compressZipToOutputStream0(zipEntityDTOList, "", zipOutputStream);
            zipOutputStream.flush();
        }
    }

    /**
     * ZipOutputStream压缩步骤，
     * 1：创建输出流 ZipOutputStream
     * 2：创建压缩实体 new ZipEntry(fileName)，fileName为 xxx/xxx 格式。如果是空目录，则是xxx/空目录/。如果不是空目录，则不需要单独弄一个压缩实体
     * 3：往流中添加一个压缩实体 zipOutputStream.putNextEntry(zipEntry);
     * 4：写入数据 zipOutputStream.write ，如果是空目录，则不需要写入
     * 5：关闭压缩实体 zipOutputStream.closeEntry();
     * 6：关闭输出流 zipOutputStream.flush();zipOutputStream.close();
     * @入参： zipEntityDTOList
     * @入参： parentName
     * @入参： zos
     * @throws Exception
     */
    private static void compressZipToOutputStream0(List<ZipEntityDTO> zipEntityDTOList,String parentName, ZipOutputStream zos ) throws Exception {
        if (CollectionUtils.isEmpty(zipEntityDTOList)){
            return;
        }
        byte[] buf = new byte[2*1024];
        for(ZipEntityDTO zipEntityDTO : zipEntityDTOList) {

            String name = "";
            if (StringUtils.isNotBlank(parentName)){
                name = parentName + "/" + zipEntityDTO.getFileName();
            }else{
                name = zipEntityDTO.getFileName();
            }
            if (zipEntityDTO.getContent() != null && CollectionUtils.isEmpty(zipEntityDTO.getChildren())) {
                //向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
                zos.putNextEntry(new ZipEntry(name));
                //copy文件到zip输出流中
                int len;
                try(InputStream in = zipEntityDTO.getContent()) {
                    while ((len = in.read(buf)) != -1) {
                        zos.write(buf, 0, len);
                    }
                    //System.err.println(zipEntityDTO.getFileName());
                    //Complete the entry
                    zos.closeEntry();
                }
            } else {
                List<ZipEntityDTO> children = zipEntityDTO.getChildren();
                if (CollectionUtils.isEmpty(children)) {
                    //需要保留原来的文件结构的时候，需要对空文件夹进行处理
                    //空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + "/"));
                    //没有文件，不需要文件的copy
                    //System.err.println(zipEntityDTO.getFileName());
                    zos.closeEntry();
                } else {
                    compressZipToOutputStream0(children, name, zos);
                }
            }
        }
    }

    /**
     * 把输入流压缩成zip格式的输出流
     * @入参： inputStreams  输入流
     * @入参： fileNames  文件名，顺序要和输入流中的顺序一致，作为压缩包内文件的名字，需要自带文件扩展名
     * @入参： outputStream   输出流，用于接收zip压缩包的输出流
     * @作者： 陈宏成
     * @since 2021/03/17
     */
    public static void compressZipToOutputStream(InputStream[] inputStreams,String[] fileNames, OutputStream outputStream) throws IOException {
        // 构建压缩流
        try(ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream)) {
            // Use Zip64 extensions for all entries where they are
            // required
            zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);
            // 将每个文件用ZipArchiveEntry封装
            // 再用ZipArchiveOutputStream写到压缩文件中
            for(int i = 0; i < inputStreams.length; i++) {
                // 获取输入流
                InputStream inputStream = inputStreams[i];
                // 构建单个压缩实体
                ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(fileNames[i]);
                // 放入压缩实体
                zipArchiveOutputStream.putArchiveEntry(zipArchiveEntry);
                // 写入数据
                try {
                    byte[] buffer = new byte[1024 * 16];
                    int len = -1;
                    while ((len = inputStream.read(buffer)) != -1) {
                        zipArchiveOutputStream.write(buffer, 0, len);
                    }
                    // 关闭写入压缩实体
                    zipArchiveOutputStream.closeArchiveEntry();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 关闭输入流，反正输入流读完后也没用了
                    if (inputStream != null) {
                        inputStream.close();
                    }
                }
            }
            zipArchiveOutputStream.finish();
        }
    }

    /**
     * 把文件压缩成zip格式
     *
     * @入参： files       需要压缩的文件
     * @入参： zipFilePath 压缩后的zip文件路径 ,如"D:/test/aa.zip";
     */
    public static void compressFiles2Zip(File[] files, String zipFilePath) {
        ZipArchiveOutputStream zaos = null;
        try {
            File zipFile = new File(zipFilePath);
            zaos = new ZipArchiveOutputStream(zipFile);
            // Use Zip64 extensions for all entries where they are
            // required
            zaos.setUseZip64(Zip64Mode.AsNeeded);
            // 将每个文件用ZipArchiveEntry封装
            // 再用ZipArchiveOutputStream写到压缩文件中
            for (File file : files) {
                ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(file, file.getName());
                zaos.putArchiveEntry(zipArchiveEntry);
                InputStream is = null;
                try {
                    is = new FileInputStream(file);
                    byte[] buffer = new byte[1024 * 16];
                    int len = -1;
                    while ((len = is.read(buffer)) != -1) {
                        zaos.write(buffer, 0, len);
                    }
                    zaos.closeArchiveEntry();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    if (is != null) {
                        is.close();
                    }
                }

            }
            zaos.finish();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (zaos != null) {
                    zaos.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 把zip文件解压到指定的文件夹
     *
     * @入参： zipFilePath zip文件路径, 如 "D:/test/aa.zip"
     * @入参： saveFileDir 解压后的文件存放路径, 如"D:/test/"
     */
    public static List<String> decompressZip1(String zipFilePath, String saveFileDir) {
        List<String> fileList = new ArrayList<>();
        File file = new File(zipFilePath);
        if (file.exists()) {
            InputStream is = null;
            // can read Zip archives
            ZipArchiveInputStream zais = null;
            try {
                is = new FileInputStream(file);
                zais = new ZipArchiveInputStream(is);
                ArchiveEntry archiveEntry = null;
                // 把zip包中的每个文件读取出来
                // 然后把文件写到指定的文件夹
                while ((archiveEntry = zais.getNextEntry()) != null) {
                    // 获取文件名
                    String entryFileName = archiveEntry.getName();
                    // 构造解压出来的文件存放路径
                    String entryFilePath = saveFileDir + entryFileName;
                    fileList.add(entryFilePath);
                    byte[] content = new byte[(int) archiveEntry.getSize()];
                    zais.read(content);
                    OutputStream os = null;
                    try {
                        // 把解压出来的文件写到指定路径
                        File entryFile = new File(entryFilePath);
                        os = new FileOutputStream(entryFile);
                        os.write(content);
                    } catch (IOException e) {
                        throw new IOException(e);
                    } finally {
                        if (os != null) {
                            os.flush();
                            os.close();
                        }
                    }

                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    if (zais != null) {
                        zais.close();
                    }
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return fileList;
    }

    /**
     * 把zip文件解压到指定的文件夹
     *
     * @入参： zipFilePath zip文件路径, 如 "D:/test/aa.zip"
     * @入参： saveFileDir 解压后的文件存放路径, 如"D:/test/"
     */
    public static List<String> decompressZip(String zipFilePath, String saveFileDir) {
        List<String> fileList = new ArrayList<>();
        try {
            ZipInputStream Zin = new ZipInputStream(new FileInputStream(zipFilePath));//输入源zip路径
            BufferedInputStream Bin = new BufferedInputStream(Zin);
            File Fout = null;
            ZipEntry entry;
            try {
                while ((entry = Zin.getNextEntry()) != null && !entry.isDirectory()) {
                    Fout = new File(saveFileDir, entry.getName());
                    // 构造解压出来的文件存放路径
                    String entryFilePath = saveFileDir + entry.getName();
                    fileList.add(entryFilePath);
                    if (!Fout.exists()) {
                        (new File(Fout.getParent())).mkdirs();
                    }
                    FileOutputStream out = new FileOutputStream(Fout);
                    BufferedOutputStream Bout = new BufferedOutputStream(out);
                    int b;
                    while ((b = Bin.read()) != -1) {
                        Bout.write(b);
                    }
                    Bout.close();
                    out.close();
                }
                Bin.close();
                Zin.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return fileList;
    }

    /**
     * @描述:针对文件夹的压缩
     * @入参:
     * @出参:
     * @作者: 戴伙进
     * @日期: 2020/6/6 18:09
     **/
    public static void dir2Zip(String srcDir,OutputStream out,boolean keepDirStructure){
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
            File sourceFile = new File(srcDir);
            //去掉压缩的文件夹名称
            compress(sourceFile,zos,"",keepDirStructure);
        }catch (Exception ex){
            throw new RuntimeException("zip error from ZipUtils",ex);
        }finally {
            if(zos != null){
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @描述: 批量下载, 打包成zip 本方法用在springmvc中, 直接返回给用户 内部调用 @see getZipInputStream
     * @入参: fileNodeDTOs文件节点dto列表
     * @入参: zipFileName 需要生成的压缩包名字 如xxx.zip
     * @入参: response http返回体
     * @出参:
     * @作者: 汤厚韬
     * @迁移: 余家鑫
     * @日期: 2021/1/26 上午9:48
     */
    public static void downloadZip(List<DownloadZipDTO> fileDTOs, String zipFileName) throws IOException {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        if (response == null) {
            throw new RuntimeException("HttpServletResponse为空");
        }
        // 创建一个新的 byte 数组输出流，它具有指定大小的缓冲区容量, 然后通过他创建一个新的缓冲输出流，以将数据写入指定的底层输出流, 最后生成一个zip输出流
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
        ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream);
        byte[] buffer = new byte[1024];
        Set<String> fileNameSet = new HashSet<>();
        for (DownloadZipDTO fileNodeDTO : fileDTOs) {
            if(fileNodeDTO == null || Objects.isNull(fileNodeDTO.getFileName()) || Objects.equals(fileNodeDTO.getFileName(), "")){
                continue;
            }
            // 防止文件重名导致 java.util.zip.ZipException: duplicate entry
            int suffix = 0;
            String fileName = fileNodeDTO.getFileName();
            String fileExt = fileNodeDTO.getFileExt();
            String fileNameExcludeExt = fileName;
            if (fileName.indexOf(fileExt) != -1) {
                fileNameExcludeExt = fileName.substring(0, fileName.indexOf(fileExt));
            } else {
                fileName = fileName + fileExt;
            }
            while (fileNameSet.contains(fileName)) {
                fileName = fileNameExcludeExt + suffix + fileExt;
                suffix += 1;
            }
            fileNameSet.add(fileName);
            InputStream fileInputStream = fileNodeDTO.getInputStream();
            //压缩文件内的文件名称
            zipOutputStream.putNextEntry(new ZipEntry(fileName));
            int length;
            while ((length = fileInputStream.read(buffer)) > 0) {
                //将文件读入压缩文件内
                zipOutputStream.write(buffer, 0, length);
            }
            zipOutputStream.closeEntry();
            fileInputStream.close();
        }
        zipOutputStream.close();
        bufferedOutputStream.flush();
        // ByteArrayInputStream
        InputStream is = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        Optional<InputStream> zipInputStream = Optional.ofNullable(is);
        if(!zipInputStream.isPresent()){
            throw new RuntimeException("获取文件流为空");
        }
        response.setCharacterEncoding("UTF-8");
        response.setHeader("content-Type", "application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipFileName, "UTF-8"));
        try (OutputStream os = response.getOutputStream();
             InputStream inputStream = zipInputStream.get()) {

            byte[] buffer1 = new byte[1024 * 5];
            int len = 0;
            while ((len = inputStream.read(buffer1)) > 0) {
                os.write(buffer1, 0, len);
            }
            os.flush();
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * @描述:递归压缩方法
     * @入参:
     * @出参:
     * @作者: 戴伙进
     * @日期: 2020/6/6 18:08
     **/
    private static void compress(File sourceFile,ZipOutputStream zos,String name,boolean keepDirStructure)throws Exception{
        byte[] buf = new byte[2*1024];
        if(sourceFile.isFile()){
            //向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            //copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);
            while ((len = in.read(buf)) !=-1){
                zos.write(buf,0,len);
            }
            //Complete the entry
            zos.closeEntry();
            in.close();
        }else {
            File[] listFiles = sourceFile.listFiles();
            if(listFiles == null || listFiles.length ==0){
                //需要保留原来的文件结构的时候，需要对空文件夹进行处理
                if(keepDirStructure){
                    //空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name+"/"));
                    //没有文件，不需要文件的copy
                    zos.closeEntry();
                }
            }else {
                for (File file : listFiles){
                    //是否需要保留原来的文件结构
                    if(keepDirStructure){
                        //注意：file.getName()前面需要带上父文件夹的名字加一斜杠
                        //不然最后压缩包中就不能保留原来的文件结构，即：所有文件都跑到压缩包根目录下了
                        if(StringUtils.isNotBlank(name)){
                            compress(file,zos,name+"/"+file.getName(),keepDirStructure);
                        }else {
                            compress(file,zos,file.getName(),keepDirStructure);
                        }
                    }else {
                        compress(file,zos,file.getName(),keepDirStructure);
                    }
                }
            }
        }
    }

    /**
     * @描述:批量文件压缩
     * @入参:
     * @出参:
     * @作者: 戴伙进
     * @日期: 2020/6/6 17:52
     **/
    public static void zip(String outZipFilename, List<String> inputFiles) {
        File[] files = new File[inputFiles.size()];
        for (int i = 0; i < inputFiles.size(); i++) {
            files[i] = new File(inputFiles.get(i));
        }
        compressFiles2Zip(files, outZipFilename);
    }

    /**
     * @描述: 解压文件
     * @入参:
     * @出参:
     * @作者: 代鹏刚
     * @日期: 2021/6/15 15:00
     **/
    public static String unZipFiles(File srcFile, String destDirPath) throws Exception {
        // 开始解压
        ZipFile zipFile = null;
        String filePath = "";
        try {
            zipFile = new ZipFile(srcFile, "gbk");

            Enumeration<?> entries = zipFile.getEntries();

            while (entries.hasMoreElements()) {
                org.apache.tools.zip.ZipEntry entry = (org.apache.tools.zip.ZipEntry) entries.nextElement();
                if (entry.getName().indexOf("/")==-1){
                    return "上传格式有误";
                }
                String[] split = entry.getName().split("/");
                if (split.length == 1) {
                    filePath = split[0];
                }
                // 如果是文件夹，就创建个文件夹
                if (entry.getName().endsWith("/")) {
                    String dirPath = destDirPath + "/" + entry.getName();
                    File dir = new File(dirPath);
                    dir.mkdirs();

                } else {
                    // 如果是文件，就先创建一个文件，然后用io流把内容copy过去
                    File targetFile = new File(destDirPath + "/" + entry.getName());
                    // 保证这个文件的父文件夹必须要存在
                    if (!targetFile.getParentFile().exists()) {
                        targetFile.getParentFile().mkdirs();
                    }
                    targetFile.createNewFile();
                    // 将压缩文件内容写入到这个文件中
                    InputStream is = zipFile.getInputStream(entry);
                    FileOutputStream fos = new FileOutputStream(targetFile);
                    int len;
                    byte[] buf = new byte[1024];
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                    }
                    // 关流顺序，先打开的后关闭
                    fos.close();
                    is.close();
                }
            }
        } catch (Exception e) {
            throw e;
        } finally {
            if (zipFile != null) {
                try {
                    zipFile.close();
                } catch (IOException e) {
                    throw e;
                }
            }
        }
        return filePath;
    }
}
